package jmine.tec.persist.hibernate.proxy;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import javassist.util.proxy.ProxyFactory;
import javassist.util.proxy.ProxyObject;
import jmine.tec.utils.spring.BeanReference;

import org.hibernate.Session;
import org.hibernate.collection.AbstractPersistentCollection;
import org.hibernate.collection.PersistentCollection;
import org.hibernate.engine.CollectionKey;
import org.hibernate.engine.PersistenceContext;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.proxy.HibernateProxyHelper;

/**
 * Encapsula uma {@link PersistentCollection} para fazê-la nunca perder a sessão possibilitando serialização da mesma.
 * 
 * @author wellington.santos
 * @created May 30, 2012
 */
public abstract class AbstractCollectionProxy extends AbstractPersistentProxy<PersistentCollection> {

    private static final Class<?>[] SUPER_INTERFACES_ARRAY = { SerializableLazyProxy.class };

    private static final Field SESSION_FIELD;

    static {
        try {
            SESSION_FIELD = AbstractPersistentCollection.class.getDeclaredField("session");
            if (!SESSION_FIELD.isAccessible()) {
                SESSION_FIELD.setAccessible(true);
            }
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private PersistentCollection impl;

    private transient boolean initialized;

    /**
     * Construtor.
     * 
     * @param sessionReference referência da sessão hibernate
     * @param impl objeto lazy a encapsular
     */
    public AbstractCollectionProxy(BeanReference<? extends SessionImplementor> sessionReference, AbstractPersistentCollection impl) {
        super(sessionReference);
        this.impl = impl;
        this.initialized = impl.wasInitialized();
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    public PersistentCollection getLazyObject() {
        return this.impl;
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    public boolean isUninitialized() {
        return !this.getLazyObject().wasInitialized();
    }

    /**
     * @param collection coleção do hibernate
     * @return a sessão que ele está usando
     */
    private static final SessionImplementor getHibernateProxySession(PersistentCollection collection) {
        try {
            return (SessionImplementor) SESSION_FIELD.get(collection);
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    @Override
    protected ProxyObject createProxy() {
        Set<Class> interfaces = new HashSet<Class>(10);
        this.collectInterfaces(interfaces, this.impl.getClass());
        for (Class intf : SUPER_INTERFACES_ARRAY) {
            interfaces.add(intf);
        }
        interfaces.remove(PersistentCollection.class);
        ProxyFactory factory = new ProxyFactory();
        factory.setSuperclass(null);
        factory.setInterfaces(interfaces.toArray(new Class[interfaces.size()]));
        factory.setFilter(FinalizeFilter.INSTANCE);
        Class<?> resultClass = factory.createClass();
        try {
            ProxyObject resultObject = (ProxyObject) resultClass.newInstance();
            resultObject.setHandler(this);
            return resultObject;
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * @param interfaces interfaces a obter
     * @param target classe a consultar
     */
    private void collectInterfaces(Collection<Class> interfaces, Class target) {
        for (Class intf : target.getInterfaces()) {
            interfaces.add(intf);
        }
        if (target != Object.class) {
            this.collectInterfaces(interfaces, target.getSuperclass());
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    public Object getImplementation() {
        this.restoreSession();
        Object result = this.impl.getValue();
        this.checkInitialization();
        return result;
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    @Override
    public void restoreSession() {
        final PersistentCollection collection = this.impl;
        if (!collection.wasInitialized()) {
            SessionImplementor oldSession = getHibernateProxySession(collection);
            if (oldSession == null || oldSession.isClosed()) {
                SessionImplementor newSession = this.getSessionImplementor();
                if (oldSession == newSession || newSession.isClosed()) {
                    return;
                }
                
                collection.unsetSession(oldSession);

                PersistenceContext persistenceContext = newSession.getPersistenceContext();
                CollectionPersister collectionPersister = newSession.getFactory().getCollectionPersister(collection.getRole());
                CollectionKey collectionKey = new CollectionKey(collectionPersister, collection.getKey(), newSession.getEntityMode());
                persistenceContext.getCollectionEntries().remove(collection);
                PersistentCollection cachedCollection = (PersistentCollection) persistenceContext.getCollectionsByKey().get(collectionKey);
                Object owner = collection.getOwner();
                if (owner != null) {
                    Serializable ownerId = collectionPersister.getOwnerEntityPersister().getIdentifier(owner, newSession.getEntityMode());
                    Object newOwner = ((Session) newSession).get(HibernateProxyHelper.getClassWithoutInitializingProxy(owner), ownerId);
                    if (newOwner != null) {
                        cachedCollection = (PersistentCollection) persistenceContext.getCollectionsByKey().get(collectionKey);
                        if (cachedCollection != null && persistenceContext.containsCollection(cachedCollection)) {
                            // usa coleção existente em cache da sessão
                            this.impl = cachedCollection;
                        }
                    }
                    // a entidade não existe mais na base, deixar estourar erro de sessão perdida
                    collection.setOwner(null);
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    @Override
    protected boolean checkInitialization() {
        if (!this.initialized && this.impl.wasInitialized()) {
            this.initialized = true;
            this.onInitialized(this.impl.getValue());
            return true;
        }
        return false;
    }

}
